Skip to content

Expose @Deprecated in generated knowledge API surface #678

Description

@DemchaAV

Problem

The knowledge generator exposes deprecated public members as if they were normal
supported API.

TextOrnaments.spacedUpper(String) is deprecated in 2.4.0, but
knowledge/api/templates.md renders:

### TextOrnaments (class)
- `String upper(String value)`
- `String spacedUpper(String value)`
- `String joinPipe(String... parts)`

Nothing says which of the first two should not be used. api-query is no better:

$ node knowledge/tools/api-query/api-query.mjs --type TextOrnaments --surface templates
class com.demcha.compose.document.templates.core.text.TextOrnaments
  methods (3):
    String upper(String value)
    String spacedUpper(String value)
    String joinPipe(String... parts)

The pack exists so an agent picks the right API instead of inventing one. Here it
does the opposite of its job: the Java API marks the method correctly, and the
generated surface launders that signal away, so an agent querying knowledge/api
can actively choose the deprecated call.

This is not limited to spacedUpper — every @Deprecated public member across
the published modules is currently indistinguishable from supported API.

Desired behavior

  • detect java.lang.Deprecated from RuntimeVisibleAnnotations;
  • carry deprecated metadata into the generated API schema;
  • preserve since / forRemoval where available if practical;
  • render deprecated members clearly in knowledge/api/*.md;
  • expose the state through api-query;
  • add fixtures/tests proving deprecated methods cannot appear as ordinary API;
  • keep extract-api --check enforcing the generated result.

Acceptance example:

TextOrnaments.spacedUpper(String)
    deprecated since 2.4.0
    replacement: upper(...) + SPACED_CAPS / DocumentLetterSpacing

Do not infer replacements automatically unless they are explicitly available from
source metadata/documentation.

What the current code already gives you, and what it does not

Three findings from reading the toolchain, so the work is not re-discovered:

1. Detection is nearly free. readAnnotations already collects every
RuntimeVisibleAnnotations entry for the class and for each method — there is no
allow-list of recognised annotations. java.lang.Deprecated has RUNTIME
retention, so it is already being parsed and then discarded. The per-method map is
keyed through memberKeyForMember, which the member mapping in extract-api.mjs
already consults.

2. since / forRemoval need a parser change, small but real.
readAnnotation in lib/annotations.mjs deliberately walks past the element pairs:

/** Read one annotation's type descriptor and step over its element pairs. */
function readAnnotation(r, utf8) {
  const typeIndex = r.u2();
  const pairs = r.u2();
  for (let i = 0; i < pairs; i += 1) {
    r.skip(2);            // element_name_index
    skipElementValue(r);
  }
  return utf8.get(typeIndex) ?? null;
}

Only the descriptor survives. Both @Deprecated elements are constant-valued
(String since, boolean forRemoval), so reading them means turning
skipElementValue into a read for the constant tags and leaving the rest skipped.
Worth keeping that narrow — the file's own stance is that a parse it cannot do
must fail loudly rather than degrade to "unannotated".

3. The replacement text is NOT reachable today, which is what makes the
"do not infer" constraint the right call.
It lives only in the @deprecated
Javadoc tag. The extractor's one source-reading pass, lib/source-names.mjs,
reads the sources jar for parameter names alone, and its first act is

function stripComments(source) {
  return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
}

— so the tag is destroyed before anything looks at it. Its header also states the
invariant that nothing in that pass may add a member. Surfacing the replacement
therefore means a deliberate new seam, not a tweak, and until one exists the
acceptance example's replacement: line can only be populated for members whose
Javadoc is actually read. Emitting deprecated since 2.4.0 with no replacement
line is the correct output in the meantime; guessing one would be worse than
omitting it, since the whole value of this file is that what it says can be
trusted.

Notes on scope

  • Schema is schemaVersion: 2. Adding an optional member/type key is additive,
    but every consumer that pretty-prints members needs updating in the same change
    or the data lands somewhere nothing displays: lib/render-markdown.mjs and
    knowledge/tools/api-query/api-query.mjs at minimum.
  • knowledge/manifest.json carries no hashes or counts, only a file index, so it
    does not need regenerating for this.
  • The fixture step in .github/workflows/ci.yml compares the set of
    *.test.mjs suites against a declared list and goes red when they differ. A new
    fixture suite must be added to that list in the same commit — by design.
  • Repo-wide blast radius is small: 6 @Deprecated public members in main sources
    today, so the regenerated surfaces will not churn much.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions